@daz4126/helium 1.0.0-beta.2 → 1.0.0-beta.4
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/CLAUDE.md +1 -3
- package/examples/index.html +13 -28
- package/examples/todo.css +109 -0
- package/examples/todo.html +46 -0
- package/examples/todo.js +36 -0
- package/helium (# Edit conflict 2026-02-04 FmuZMse #).js +629 -0
- package/helium (# Edit conflict 2026-02-04 j7PIppO #).js +629 -0
- package/helium (# Edit conflict 2026-02-04 mqpbJka #).js +629 -0
- package/helium-csp.js +2 -6
- package/helium-lite.js +216 -100
- package/helium.js +17 -32
- package/package.json +1 -1
package/helium-lite.js
CHANGED
|
@@ -4,19 +4,47 @@
|
|
|
4
4
|
* No AJAX, no @import, no SSE, no Turbo
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const parseEx =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
const parseEx = (v) => {
|
|
8
|
+
try {
|
|
9
|
+
return Function(`return(${v})`)();
|
|
10
|
+
} catch {
|
|
11
|
+
return v;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const tryNum = (v) => {
|
|
15
|
+
const t = String(v).trim(),
|
|
16
|
+
n = +t;
|
|
17
|
+
return t && !isNaN(n) ? n : v;
|
|
18
|
+
};
|
|
19
|
+
const isValidIdentifier = (v) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(v);
|
|
20
|
+
const RESERVED = new Set([
|
|
21
|
+
"undefined",
|
|
22
|
+
"null",
|
|
23
|
+
"true",
|
|
24
|
+
"false",
|
|
25
|
+
"NaN",
|
|
26
|
+
"Infinity",
|
|
27
|
+
"this",
|
|
28
|
+
"arguments",
|
|
29
|
+
]);
|
|
30
|
+
const KEY_MODS = {
|
|
31
|
+
shift: "shiftKey",
|
|
32
|
+
ctrl: "ctrlKey",
|
|
33
|
+
alt: "altKey",
|
|
34
|
+
meta: "metaKey",
|
|
35
|
+
};
|
|
36
|
+
const debounce = (f, d) => {
|
|
37
|
+
let t;
|
|
38
|
+
return (...a) => (clearTimeout(t), (t = setTimeout(f, d, ...a)));
|
|
39
|
+
};
|
|
13
40
|
|
|
14
41
|
const ATTR_REGEX = /^(@|:|data-he)/;
|
|
15
42
|
|
|
16
43
|
const match = (name, ...attrs) => {
|
|
17
44
|
const p = name.split(/[.:]/)[0];
|
|
18
|
-
|
|
19
|
-
|
|
45
|
+
return p == ":" || p == ""
|
|
46
|
+
? false
|
|
47
|
+
: attrs.some((a) => p === `@${a}` || p === `data-he-${a}`);
|
|
20
48
|
};
|
|
21
49
|
|
|
22
50
|
let HELIUM = null;
|
|
@@ -24,8 +52,13 @@ let HELIUM = null;
|
|
|
24
52
|
async function helium(initialState = {}) {
|
|
25
53
|
const inits = [];
|
|
26
54
|
const ALL = Symbol("all");
|
|
27
|
-
const root =
|
|
28
|
-
|
|
55
|
+
const root =
|
|
56
|
+
document.querySelector("[\\@helium]") ||
|
|
57
|
+
document.querySelector("[data-helium]") ||
|
|
58
|
+
document.body;
|
|
59
|
+
const storageKey =
|
|
60
|
+
root.getAttribute("@local-storage") ||
|
|
61
|
+
root.getAttribute("data-he-local-storage");
|
|
29
62
|
|
|
30
63
|
if (!HELIUM) {
|
|
31
64
|
HELIUM = {
|
|
@@ -36,12 +69,14 @@ async function helium(initialState = {}) {
|
|
|
36
69
|
processed: new WeakSet(),
|
|
37
70
|
parentKeys: new WeakMap(),
|
|
38
71
|
fnCache: new Map(),
|
|
39
|
-
proxyCache: new WeakMap()
|
|
72
|
+
proxyCache: new WeakMap(),
|
|
40
73
|
};
|
|
41
74
|
}
|
|
42
75
|
|
|
43
|
-
const $ = s => document.querySelector(s);
|
|
44
|
-
const html =
|
|
76
|
+
const $ = (s) => document.querySelector(s);
|
|
77
|
+
const html = (s) =>
|
|
78
|
+
Object.assign(document.createElement("template"), { innerHTML: s.trim() })
|
|
79
|
+
.content.firstChild;
|
|
45
80
|
|
|
46
81
|
const applyingBindings = new Set();
|
|
47
82
|
|
|
@@ -57,9 +92,9 @@ async function helium(initialState = {}) {
|
|
|
57
92
|
|
|
58
93
|
const handler = {
|
|
59
94
|
has(t, p) {
|
|
60
|
-
if (typeof p ===
|
|
95
|
+
if (typeof p === "string" && p.startsWith("$")) return false;
|
|
61
96
|
if (Reflect.has(t, p)) return true;
|
|
62
|
-
if (typeof globalThis[p] !==
|
|
97
|
+
if (typeof globalThis[p] !== "undefined") return false;
|
|
63
98
|
return true;
|
|
64
99
|
},
|
|
65
100
|
get(t, p, r) {
|
|
@@ -81,21 +116,16 @@ async function helium(initialState = {}) {
|
|
|
81
116
|
HELIUM.bindings.get(ALL)?.forEach(safeApplyBinding);
|
|
82
117
|
if (storageKey) localStorage.setItem(storageKey, JSON.stringify(state));
|
|
83
118
|
return res;
|
|
84
|
-
}
|
|
119
|
+
},
|
|
85
120
|
};
|
|
86
121
|
|
|
87
122
|
const state = new Proxy({}, handler);
|
|
88
|
-
if (storageKey)
|
|
123
|
+
if (storageKey)
|
|
124
|
+
try {
|
|
125
|
+
Object.assign(state, JSON.parse(localStorage.getItem(storageKey)));
|
|
126
|
+
} catch {}
|
|
89
127
|
Object.assign(state, initialState);
|
|
90
128
|
|
|
91
|
-
const getRefsObject = () => {
|
|
92
|
-
const refs = {};
|
|
93
|
-
for (const [name, element] of HELIUM.refs) {
|
|
94
|
-
refs[name] = element;
|
|
95
|
-
}
|
|
96
|
-
return refs;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
129
|
const createScope = (el, event = {}) => {
|
|
100
130
|
return {
|
|
101
131
|
$,
|
|
@@ -103,7 +133,7 @@ async function helium(initialState = {}) {
|
|
|
103
133
|
$event: event,
|
|
104
134
|
$el: el,
|
|
105
135
|
$html: html,
|
|
106
|
-
...
|
|
136
|
+
...Object.fromEntries(HELIUM.refs),
|
|
107
137
|
};
|
|
108
138
|
};
|
|
109
139
|
|
|
@@ -115,7 +145,7 @@ async function helium(initialState = {}) {
|
|
|
115
145
|
"$scope",
|
|
116
146
|
withReturn
|
|
117
147
|
? `with($scope){with($scope.$data){return(${expr.trim()})}}`
|
|
118
|
-
: `with($scope){with($scope.$data){${expr.trim()}}}
|
|
148
|
+
: `with($scope){with($scope.$data){${expr.trim()}}}`,
|
|
119
149
|
);
|
|
120
150
|
const compiled = { execute: (scope) => fn(scope), getIds: null };
|
|
121
151
|
HELIUM.fnCache.set(key, compiled);
|
|
@@ -136,18 +166,22 @@ async function helium(initialState = {}) {
|
|
|
136
166
|
if (prop === "innerHTML") {
|
|
137
167
|
const content = Array.isArray(r) ? r.join`` : r;
|
|
138
168
|
return typeof Idiomorph === "object"
|
|
139
|
-
? Idiomorph.morph(el, content, { morphStyle:
|
|
140
|
-
: el.innerHTML = content;
|
|
169
|
+
? Idiomorph.morph(el, content, { morphStyle: "innerHTML" })
|
|
170
|
+
: (el.innerHTML = content);
|
|
141
171
|
}
|
|
142
172
|
if (prop === "class" && r && typeof r === "object")
|
|
143
173
|
return Object.entries(r).forEach(([k, v]) =>
|
|
144
|
-
k.split(/\s+/).forEach(c => el.classList.toggle(c, v))
|
|
174
|
+
k.split(/\s+/).forEach((c) => el.classList.toggle(c, v)),
|
|
175
|
+
);
|
|
145
176
|
|
|
146
177
|
if (prop === "style" && r && typeof r === "object")
|
|
147
|
-
return el.style.cssText = Object.entries(r)
|
|
178
|
+
return (el.style.cssText = Object.entries(r)
|
|
148
179
|
.filter(([_, v]) => v != null && v !== false)
|
|
149
|
-
.map(
|
|
150
|
-
|
|
180
|
+
.map(
|
|
181
|
+
([k, v]) =>
|
|
182
|
+
`${k.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase())}:${v}`,
|
|
183
|
+
)
|
|
184
|
+
.join("; "));
|
|
151
185
|
|
|
152
186
|
if (prop === "hidden") {
|
|
153
187
|
el.hidden = isHiddenAttr ? !!r : !r;
|
|
@@ -156,8 +190,9 @@ async function helium(initialState = {}) {
|
|
|
156
190
|
|
|
157
191
|
if (prop in el) {
|
|
158
192
|
if (isRadio && prop !== "checked") el.checked = el.value === r;
|
|
159
|
-
else if (el.type === "radio" && prop !== "checked")
|
|
160
|
-
|
|
193
|
+
else if (el.type === "radio" && prop !== "checked")
|
|
194
|
+
el.checked = el.value === r;
|
|
195
|
+
else el[prop] = prop === "textContent" ? (r ?? "") : parseEx(r);
|
|
161
196
|
return;
|
|
162
197
|
}
|
|
163
198
|
|
|
@@ -171,12 +206,16 @@ async function helium(initialState = {}) {
|
|
|
171
206
|
return true;
|
|
172
207
|
},
|
|
173
208
|
get(target, prop) {
|
|
174
|
-
if (typeof prop ==
|
|
175
|
-
excludeChanged
|
|
209
|
+
if (typeof prop == "string") {
|
|
210
|
+
excludeChanged
|
|
211
|
+
? !accessed.has(prop) && accessed.set(prop, target[prop])
|
|
212
|
+
: accessed.add(prop);
|
|
176
213
|
}
|
|
177
214
|
const val = target[prop];
|
|
178
|
-
return typeof val == "object" && val != null
|
|
179
|
-
|
|
215
|
+
return typeof val == "object" && val != null
|
|
216
|
+
? new Proxy(val, this)
|
|
217
|
+
: val;
|
|
218
|
+
},
|
|
180
219
|
});
|
|
181
220
|
|
|
182
221
|
try {
|
|
@@ -186,21 +225,29 @@ async function helium(initialState = {}) {
|
|
|
186
225
|
$event: {},
|
|
187
226
|
$el: el,
|
|
188
227
|
$html: html,
|
|
189
|
-
...
|
|
228
|
+
...Object.fromEntries(HELIUM.refs),
|
|
190
229
|
};
|
|
191
230
|
compiled.execute(scope);
|
|
192
231
|
} catch {}
|
|
193
232
|
|
|
194
|
-
return excludeChanged
|
|
233
|
+
return excludeChanged
|
|
234
|
+
? [...accessed.keys()].filter(
|
|
235
|
+
(prop) => state[prop] === accessed.get(prop),
|
|
236
|
+
)
|
|
237
|
+
: [...accessed];
|
|
195
238
|
};
|
|
196
239
|
|
|
197
240
|
const getDependencies = (compiled, el, excludeChanged = false) => {
|
|
198
241
|
return trackDependenciesProxy(compiled, el, excludeChanged);
|
|
199
242
|
};
|
|
200
243
|
|
|
201
|
-
const cleanup = el => {
|
|
202
|
-
[el, ...el.querySelectorAll(
|
|
203
|
-
HELIUM.listeners
|
|
244
|
+
const cleanup = (el) => {
|
|
245
|
+
[el, ...el.querySelectorAll("*")].forEach((e) => {
|
|
246
|
+
HELIUM.listeners
|
|
247
|
+
.get(e)
|
|
248
|
+
?.forEach(({ receiver, event, handler }) =>
|
|
249
|
+
receiver.removeEventListener(event, handler),
|
|
250
|
+
);
|
|
204
251
|
HELIUM.listeners.delete(e);
|
|
205
252
|
});
|
|
206
253
|
};
|
|
@@ -209,91 +256,154 @@ async function helium(initialState = {}) {
|
|
|
209
256
|
const newBindings = [];
|
|
210
257
|
const deferredBindings = [];
|
|
211
258
|
|
|
212
|
-
const heElements = [element, ...element.querySelectorAll("*")].filter(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
259
|
+
const heElements = [element, ...element.querySelectorAll("*")].filter(
|
|
260
|
+
(e) => {
|
|
261
|
+
if (HELIUM.processed.has(e)) return false;
|
|
262
|
+
for (let i = 0; i < e.attributes.length; i++) {
|
|
263
|
+
if (ATTR_REGEX.test(e.attributes[i].name)) return true;
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
},
|
|
267
|
+
);
|
|
219
268
|
|
|
220
269
|
const addBinding = (val, b) => {
|
|
221
|
-
HELIUM.bindings.set(
|
|
270
|
+
HELIUM.bindings.set(
|
|
271
|
+
val,
|
|
272
|
+
b.calc
|
|
273
|
+
? [b, ...(HELIUM.bindings.get(val) || [])]
|
|
274
|
+
: [...(HELIUM.bindings.get(val) || []), b],
|
|
275
|
+
);
|
|
222
276
|
b.calc ? newBindings.unshift(b) : newBindings.push(b);
|
|
223
277
|
};
|
|
224
278
|
|
|
225
|
-
heElements.forEach(el => {
|
|
279
|
+
heElements.forEach((el) => {
|
|
226
280
|
HELIUM.processed.add(el);
|
|
227
281
|
|
|
228
282
|
const attrs = el.attributes;
|
|
229
283
|
const inputType = el.type?.toLowerCase();
|
|
230
|
-
const isCheckbox = inputType == "checkbox",
|
|
284
|
+
const isCheckbox = inputType == "checkbox",
|
|
285
|
+
isRadio = inputType == "radio",
|
|
286
|
+
isSelect = el.tagName == "SELECT";
|
|
231
287
|
|
|
232
288
|
for (let i = 0; i < attrs.length; i++) {
|
|
233
289
|
const { name, value } = attrs[i];
|
|
234
290
|
|
|
235
|
-
if (
|
|
291
|
+
if (
|
|
292
|
+
!name.startsWith("@") &&
|
|
293
|
+
!name.startsWith(":") &&
|
|
294
|
+
!name.startsWith("data-he")
|
|
295
|
+
)
|
|
296
|
+
continue;
|
|
236
297
|
|
|
237
298
|
// Initialize state first if needed (skip reserved words, only if not already set)
|
|
238
|
-
if (
|
|
239
|
-
|
|
299
|
+
if (
|
|
300
|
+
match(name, "text", "html", "bind") &&
|
|
301
|
+
isValidIdentifier(value) &&
|
|
302
|
+
!RESERVED.has(value) &&
|
|
303
|
+
state[value] == null
|
|
304
|
+
) {
|
|
305
|
+
state[value] = match(name, "bind")
|
|
306
|
+
? el.type == "checkbox"
|
|
307
|
+
? el.checked
|
|
308
|
+
: tryNum(el.value)
|
|
309
|
+
: tryNum(el.textContent);
|
|
240
310
|
}
|
|
241
311
|
|
|
242
312
|
// Process the attribute
|
|
243
313
|
if (name === "@data" || name === "data-he") {
|
|
244
314
|
Object.assign(state, parseEx(value));
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
315
|
+
} else if (name.startsWith(":") || name.startsWith("data-he-attr:")) {
|
|
316
|
+
const propName = name.startsWith(":")
|
|
317
|
+
? name.slice(1)
|
|
318
|
+
: name.slice(13);
|
|
319
|
+
deferredBindings.push({
|
|
320
|
+
el,
|
|
321
|
+
prop: propName,
|
|
322
|
+
compiled: compile(value, true),
|
|
323
|
+
});
|
|
324
|
+
} else if (match(name, "ref")) {
|
|
251
325
|
HELIUM.refs.set("$" + value, el);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
326
|
+
} else if (match(name, "text", "html")) {
|
|
327
|
+
deferredBindings.push({
|
|
328
|
+
el,
|
|
329
|
+
prop: match(name, "text") ? "textContent" : "innerHTML",
|
|
330
|
+
compiled: compile(value, true),
|
|
331
|
+
});
|
|
332
|
+
} else if (match(name, "bind")) {
|
|
333
|
+
const event = isCheckbox || isRadio || isSelect ? "change" : "input";
|
|
258
334
|
const prop = isCheckbox ? "checked" : "value";
|
|
259
|
-
const inputHandler = e =>
|
|
335
|
+
const inputHandler = (e) =>
|
|
336
|
+
(state[value] = isCheckbox ? e.target.checked : e.target.value);
|
|
260
337
|
el.addEventListener(event, inputHandler);
|
|
261
338
|
if (!HELIUM.listeners.has(el)) HELIUM.listeners.set(el, []);
|
|
262
|
-
HELIUM.listeners
|
|
263
|
-
|
|
339
|
+
HELIUM.listeners
|
|
340
|
+
.get(el)
|
|
341
|
+
.push({ receiver: el, event, handler: inputHandler });
|
|
342
|
+
deferredBindings.push({
|
|
343
|
+
el,
|
|
344
|
+
prop,
|
|
345
|
+
compiled: compile(value, true),
|
|
346
|
+
isRadio,
|
|
347
|
+
});
|
|
264
348
|
if (isCheckbox) el.checked = !!state[value];
|
|
265
349
|
else if (isRadio) el.checked = el.value == state[value];
|
|
266
350
|
else el.value = state[value] ?? "";
|
|
267
|
-
}
|
|
268
|
-
else if (match(name, "hidden", "visible")) {
|
|
351
|
+
} else if (match(name, "hidden", "visible")) {
|
|
269
352
|
const isHidden = match(name, "hidden");
|
|
270
|
-
deferredBindings.push({
|
|
271
|
-
|
|
272
|
-
|
|
353
|
+
deferredBindings.push({
|
|
354
|
+
el,
|
|
355
|
+
prop: "hidden",
|
|
356
|
+
compiled: compile(value, true),
|
|
357
|
+
isHiddenAttr: isHidden,
|
|
358
|
+
});
|
|
359
|
+
} else if (match(name, "calculate")) {
|
|
273
360
|
const calcName = name.split(":")[1];
|
|
274
361
|
if (!(calcName in state)) state[calcName] = undefined;
|
|
275
|
-
deferredBindings.push({
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
362
|
+
deferredBindings.push({
|
|
363
|
+
el,
|
|
364
|
+
calc: calcName,
|
|
365
|
+
prop: null,
|
|
366
|
+
compiled: compile(value, true),
|
|
367
|
+
});
|
|
368
|
+
} else if (match(name, "effect")) {
|
|
369
|
+
deferredBindings.push({
|
|
370
|
+
el,
|
|
371
|
+
prop: null,
|
|
372
|
+
compiled: compile(value, true),
|
|
373
|
+
keys: name.split(":").slice(1),
|
|
374
|
+
});
|
|
375
|
+
} else if (match(name, "init")) {
|
|
281
376
|
inits.push({ compiled: compile(value, true), el });
|
|
282
|
-
}
|
|
283
|
-
else if (name.startsWith("@") || name.startsWith("data-he")) {
|
|
377
|
+
} else if (name.startsWith("@") || name.startsWith("data-he")) {
|
|
284
378
|
const fullName = name.startsWith("@") ? name.slice(1) : name.slice(8);
|
|
285
379
|
const [event, ...mods] = fullName.split(".");
|
|
286
|
-
const receiver =
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
380
|
+
const receiver =
|
|
381
|
+
mods.includes("outside") || mods.includes("document")
|
|
382
|
+
? document
|
|
383
|
+
: el;
|
|
384
|
+
const debounceMod = mods.find((m) => m.startsWith("debounce"));
|
|
385
|
+
const debounceDelay = debounceMod
|
|
386
|
+
? ((t) => (t && !isNaN(t) ? Number(t) : 300))(
|
|
387
|
+
debounceMod.split(":")[1],
|
|
388
|
+
)
|
|
389
|
+
: 0;
|
|
390
|
+
const _handler = (e) => {
|
|
290
391
|
if (mods.includes("prevent")) e.preventDefault();
|
|
291
392
|
if (mods.includes("stop")) e.stopPropagation();
|
|
292
|
-
for (const [mod, prop] of Object.entries(KEY_MODS))
|
|
393
|
+
for (const [mod, prop] of Object.entries(KEY_MODS))
|
|
394
|
+
if (mods.includes(mod) && !e[prop]) return;
|
|
293
395
|
if (["keydown", "keyup", "keypress"].includes(event)) {
|
|
294
396
|
const last = mods[mods.length - 1];
|
|
295
|
-
if (
|
|
296
|
-
|
|
397
|
+
if (
|
|
398
|
+
last &&
|
|
399
|
+
!["prevent", "once", "outside", "document", "stop"].includes(
|
|
400
|
+
last,
|
|
401
|
+
) &&
|
|
402
|
+
!last.startsWith("debounce") &&
|
|
403
|
+
!Object.keys(KEY_MODS).includes(last)
|
|
404
|
+
) {
|
|
405
|
+
const keyName =
|
|
406
|
+
e.key == " " ? "Space" : e.key == "Escape" ? "Esc" : e.key;
|
|
297
407
|
if (keyName.toLowerCase() !== last.toLowerCase()) return;
|
|
298
408
|
}
|
|
299
409
|
}
|
|
@@ -302,10 +412,14 @@ async function helium(initialState = {}) {
|
|
|
302
412
|
const compiled = compile(value, true);
|
|
303
413
|
compiled.execute(scope);
|
|
304
414
|
}
|
|
305
|
-
if (mods.includes("once"))
|
|
415
|
+
if (mods.includes("once"))
|
|
416
|
+
receiver.removeEventListener(event, handler);
|
|
306
417
|
};
|
|
307
|
-
const handler =
|
|
308
|
-
|
|
418
|
+
const handler =
|
|
419
|
+
debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
|
|
420
|
+
const eventOptions = mods.includes("once")
|
|
421
|
+
? { once: true }
|
|
422
|
+
: undefined;
|
|
309
423
|
receiver.addEventListener(event, handler, eventOptions);
|
|
310
424
|
if (!HELIUM.listeners.has(el)) HELIUM.listeners.set(el, []);
|
|
311
425
|
HELIUM.listeners.get(el).push({ receiver, event, handler });
|
|
@@ -313,9 +427,11 @@ async function helium(initialState = {}) {
|
|
|
313
427
|
}
|
|
314
428
|
});
|
|
315
429
|
|
|
316
|
-
deferredBindings.forEach(b => {
|
|
317
|
-
const tracked = b.keys?.includes("*")
|
|
318
|
-
|
|
430
|
+
deferredBindings.forEach((b) => {
|
|
431
|
+
const tracked = b.keys?.includes("*")
|
|
432
|
+
? [ALL]
|
|
433
|
+
: getDependencies(b.compiled, b.el, true).concat(b.keys || []);
|
|
434
|
+
tracked.forEach((key) => addBinding(key, b));
|
|
319
435
|
if (tracked.length === 0) {
|
|
320
436
|
newBindings.push(b);
|
|
321
437
|
}
|
|
@@ -355,13 +471,13 @@ function heliumTeardown() {
|
|
|
355
471
|
}
|
|
356
472
|
|
|
357
473
|
// Browser globals
|
|
358
|
-
if (typeof window !==
|
|
474
|
+
if (typeof window !== "undefined") {
|
|
359
475
|
window.helium = helium;
|
|
360
476
|
window.heliumTeardown = heliumTeardown;
|
|
361
477
|
}
|
|
362
478
|
|
|
363
479
|
// Auto-initialize
|
|
364
|
-
if (typeof document !==
|
|
480
|
+
if (typeof document !== "undefined") {
|
|
365
481
|
document.addEventListener("DOMContentLoaded", () => helium());
|
|
366
482
|
}
|
|
367
483
|
|
package/helium.js
CHANGED
|
@@ -46,11 +46,9 @@ const defaultEngine = {
|
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
// Factory function to create helium with custom engine
|
|
49
|
+
// Factory function to create helium with custom engine
|
|
50
50
|
export function createHelium(options = {}) {
|
|
51
51
|
const engine = options.engine ?? defaultEngine;
|
|
52
|
-
const prefix = options.prefix ?? 'he'; // 'he' or 'xe'
|
|
53
|
-
const rootAttr = options.rootAttr ?? 'helium'; // 'helium' or 'xenon'
|
|
54
52
|
|
|
55
53
|
// Engine-aware expression parser (CSP-safe when using jexpr engine)
|
|
56
54
|
const evalExpr = v => {
|
|
@@ -61,13 +59,10 @@ export function createHelium(options = {}) {
|
|
|
61
59
|
} catch { return v; }
|
|
62
60
|
};
|
|
63
61
|
|
|
64
|
-
// Build prefix-aware regex and matcher
|
|
65
|
-
const ATTR_REGEX = new RegExp(`^(@|:|data-${prefix})`);
|
|
66
|
-
|
|
67
62
|
const match = (name, ...attrs) => {
|
|
68
63
|
const p = name.split(/[.:]/)[0];
|
|
69
64
|
if (p === ":" || p === "") return false;
|
|
70
|
-
return attrs.some(a => p === `@${a}` || p ===
|
|
65
|
+
return attrs.some(a => p === `@${a}` || p === 'data-he-' + a);
|
|
71
66
|
};
|
|
72
67
|
|
|
73
68
|
// Single global object to hold all settings (scoped to this instance)
|
|
@@ -76,8 +71,8 @@ export function createHelium(options = {}) {
|
|
|
76
71
|
async function helium(initialState = {}) {
|
|
77
72
|
const inits = [];
|
|
78
73
|
const ALL = Symbol("all");
|
|
79
|
-
const root = document.querySelector(
|
|
80
|
-
const storageKey = root.getAttribute('@local-storage') || root.getAttribute(
|
|
74
|
+
const root = document.querySelector('[\\@helium]') || document.querySelector('[data-helium]') || document.body;
|
|
75
|
+
const storageKey = root.getAttribute('@local-storage') || root.getAttribute('data-he-local-storage');
|
|
81
76
|
|
|
82
77
|
// Initialize or reuse HELIUM object
|
|
83
78
|
if (!HELIUM) {
|
|
@@ -249,15 +244,6 @@ export function createHelium(options = {}) {
|
|
|
249
244
|
// Merge initial state
|
|
250
245
|
Object.assign(state, initialState);
|
|
251
246
|
|
|
252
|
-
// Build refs object for scope
|
|
253
|
-
const getRefsObject = () => {
|
|
254
|
-
const refs = {};
|
|
255
|
-
for (const [name, element] of HELIUM.refs) {
|
|
256
|
-
refs[name] = element;
|
|
257
|
-
}
|
|
258
|
-
return refs;
|
|
259
|
-
};
|
|
260
|
-
|
|
261
247
|
// Create scope for expression execution
|
|
262
248
|
const createScope = (el, event = {}) => {
|
|
263
249
|
return engine.createScope({
|
|
@@ -271,7 +257,7 @@ export function createHelium(options = {}) {
|
|
|
271
257
|
put,
|
|
272
258
|
patch,
|
|
273
259
|
del,
|
|
274
|
-
refs:
|
|
260
|
+
refs: Object.fromEntries(HELIUM.refs)
|
|
275
261
|
});
|
|
276
262
|
};
|
|
277
263
|
|
|
@@ -354,7 +340,7 @@ export function createHelium(options = {}) {
|
|
|
354
340
|
put,
|
|
355
341
|
patch,
|
|
356
342
|
del,
|
|
357
|
-
refs:
|
|
343
|
+
refs: Object.fromEntries(HELIUM.refs)
|
|
358
344
|
});
|
|
359
345
|
compiled.execute(scope);
|
|
360
346
|
} catch {}
|
|
@@ -387,7 +373,8 @@ export function createHelium(options = {}) {
|
|
|
387
373
|
const heElements = [element, ...element.querySelectorAll("*")].filter(e => {
|
|
388
374
|
if (HELIUM.processed.has(e)) return false;
|
|
389
375
|
for (let i = 0; i < e.attributes.length; i++) {
|
|
390
|
-
|
|
376
|
+
const n = e.attributes[i].name;
|
|
377
|
+
if (n[0] === '@' || n[0] === ':' || n.startsWith('data-he')) return true;
|
|
391
378
|
}
|
|
392
379
|
return false;
|
|
393
380
|
});
|
|
@@ -400,7 +387,7 @@ export function createHelium(options = {}) {
|
|
|
400
387
|
const importPromises = [];
|
|
401
388
|
|
|
402
389
|
heElements.forEach((el) => {
|
|
403
|
-
const importAttr = el.getAttribute("@import") || el.getAttribute(
|
|
390
|
+
const importAttr = el.getAttribute("@import") || el.getAttribute('data-he-import');
|
|
404
391
|
if (importAttr) {
|
|
405
392
|
importAttr.split(",").map((m) => m.trim()).forEach((moduleName) => {
|
|
406
393
|
// Build path: use as-is for URLs, otherwise resolve relative to document location
|
|
@@ -408,9 +395,7 @@ export function createHelium(options = {}) {
|
|
|
408
395
|
const hasExtension = moduleName.endsWith(".js");
|
|
409
396
|
const hasPathPrefix = moduleName.startsWith("/") || moduleName.startsWith("./") || moduleName.startsWith("../");
|
|
410
397
|
const relativePath = (isUrl || hasPathPrefix ? "" : "./") + moduleName + (isUrl || hasExtension ? "" : ".js");
|
|
411
|
-
|
|
412
|
-
const baseUrl = location.href.endsWith('/') || location.href.includes('.html') ? location.href : location.href + '/';
|
|
413
|
-
const path = isUrl ? relativePath : new URL(relativePath, baseUrl).href;
|
|
398
|
+
const path = isUrl ? moduleName : new URL(relativePath, location.href).href;
|
|
414
399
|
importPromises.push(
|
|
415
400
|
import(path)
|
|
416
401
|
.then((module) => {
|
|
@@ -448,7 +433,7 @@ export function createHelium(options = {}) {
|
|
|
448
433
|
const {name, value} = attrs[i];
|
|
449
434
|
|
|
450
435
|
// Skip non-helium attributes early
|
|
451
|
-
if (
|
|
436
|
+
if (name[0] !== '@' && name[0] !== ':' && !name.startsWith('data-he')) continue;
|
|
452
437
|
|
|
453
438
|
// Initialize state first if needed (skip reserved words, only if not already set)
|
|
454
439
|
if (match(name, "text", "html", "bind") && isValidIdentifier(value) && !RESERVED.has(value) && state[value] == null) {
|
|
@@ -456,11 +441,11 @@ export function createHelium(options = {}) {
|
|
|
456
441
|
}
|
|
457
442
|
|
|
458
443
|
// Process the attribute
|
|
459
|
-
if (name === "@data" || name ===
|
|
444
|
+
if (name === "@data" || name === 'data-he') {
|
|
460
445
|
Object.assign(state, evalExpr(value));
|
|
461
446
|
}
|
|
462
|
-
else if (name.startsWith(":") || name.startsWith(
|
|
463
|
-
const propName = name.startsWith(":") ? name.slice(1) : name.slice(
|
|
447
|
+
else if (name.startsWith(":") || name.startsWith('data-he-attr:')) {
|
|
448
|
+
const propName = name.startsWith(":") ? name.slice(1) : name.slice(13);
|
|
464
449
|
deferredBindings.push({el, prop: propName, compiled: compile(value, true)});
|
|
465
450
|
}
|
|
466
451
|
else if (match(name, "ref")) {
|
|
@@ -496,8 +481,8 @@ export function createHelium(options = {}) {
|
|
|
496
481
|
else if (match(name, "init")) {
|
|
497
482
|
inits.push({ compiled: compile(value, true), el });
|
|
498
483
|
}
|
|
499
|
-
else if (name.startsWith("@") || name.startsWith(
|
|
500
|
-
const fullName = name.startsWith("@") ? name.slice(1) : name.slice(
|
|
484
|
+
else if (name.startsWith("@") || name.startsWith('data-he')) {
|
|
485
|
+
const fullName = name.startsWith("@") ? name.slice(1) : name.slice(8);
|
|
501
486
|
const [eventName, ...mods] = fullName.split(".");
|
|
502
487
|
const isHttpMethod = ["get", "post", "put", "patch", "delete"].includes(eventName);
|
|
503
488
|
const event = isHttpMethod ? getEvent(el) : eventName;
|
|
@@ -517,7 +502,7 @@ export function createHelium(options = {}) {
|
|
|
517
502
|
}
|
|
518
503
|
if (!mods.includes("outside") || !el.contains(e.target)) {
|
|
519
504
|
if (isHttpMethod) {
|
|
520
|
-
const getAttr =
|
|
505
|
+
const getAttr = a => el.getAttribute('data-he-' + a) || el.getAttribute('@' + a);
|
|
521
506
|
const pairs = (getAttr('target') || "").split(",").map(p => p.split(":").map(s => s.trim()));
|
|
522
507
|
const target = pairs.map(([target]) => target);
|
|
523
508
|
const action = pairs.map(([, action]) => action);
|